Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
- Replace inline gallery/timeline types in ListViewSchema with GalleryConfig/TimelineConfig references - Add ListViewGalleryConfig and ListViewTimelineConfig intersection types with legacy field support - Fix ListView to pass gallery as nested object so ObjectGallery reads spec props correctly - Add comprehensive spec config tests for gallery (coverField, cardSize, coverFit, visibleFields) - Add spec config type tests for timeline (startDateField, scale, groupByField, etc.) - Add backward compatibility tests for legacy options.gallery/timeline Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
|
@copilot 仔细检查,需要完成任务中所有的开发 |
…timeline as nested object - ObjectTimeline now supports `schema.timeline.*` nested config with fallback to flat props - ObjectTimeline maps `timeline.scale` to `timeScale` for the renderer - ListView timeline case mirrors gallery pattern: passes nested `timeline` config - Bridge uses strong ListViewGalleryConfig/ListViewTimelineConfig types - Added 13 ObjectTimeline spec config tests (startDateField, endDateField, groupByField, colorField, scale, backward compat, spec priority) - Enhanced GalleryTimelineSpecConfig tests with cardSize/coverFit/co-existence coverage Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
… any casts Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Completed all remaining development tasks in commit
All 388 tests pass (318 plugin + 70 bridge). |
There was a problem hiding this comment.
Pull request overview
This PR standardizes Gallery and Timeline spec config integration across ObjectUI by replacing legacy flat prop spreading with nested config objects and strong types from @objectstack/spec/ui. It fixes a bug where ListView.tsx was spreading config flat instead of passing nested objects, causing spec properties like cardSize, coverFit, and visibleFields to be lost for Gallery, and timeline config to not propagate correctly to ObjectTimeline.
Changes:
- Introduced
ListViewGalleryConfigandListViewTimelineConfigintersection types combining spec-canonical types with legacy fields for backward compatibility - Refactored
ListView.tsxto pass gallery and timeline configs as nested objects instead of flat spreads - Updated
ObjectTimelineto accept and prioritize nestedschema.timeline.*config with proper fallback chains - Added comprehensive test coverage (35 new tests across gallery, timeline, and type validation)
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
packages/types/src/objectql.ts |
Defines ListViewGalleryConfig and ListViewTimelineConfig intersection types extending spec types with legacy fields and index signatures |
packages/types/src/index.ts |
Exports the new ListViewGalleryConfig and ListViewTimelineConfig types |
packages/react/src/spec-bridge/bridges/list-view.ts |
Updates spec bridge from any to strong ListViewGalleryConfig/ListViewTimelineConfig types |
packages/plugin-list/src/ListView.tsx |
Refactors gallery and timeline cases to merge spec config over legacy options and pass as nested objects with deprecated flat prop fallbacks |
packages/plugin-list/src/__tests__/ObjectGallery.test.tsx |
Adds 12 spec config integration tests covering coverField, cardSize, coverFit, visibleFields, titleField, and legacy fallbacks |
packages/plugin-list/src/__tests__/GalleryTimelineSpecConfig.test.tsx |
Adds 10 type-level tests validating ListViewSchema accepts spec config, legacy fields, and co-existence scenarios |
packages/plugin-timeline/src/ObjectTimeline.tsx |
Implements nested timeline config support with fallback chain: timeline.* → mapping.* → flat props → defaults |
packages/plugin-timeline/src/__tests__/ObjectTimeline.spec-config.test.tsx |
Adds 13 spec config tests for startDateField, endDateField, groupByField, colorField, scale, and backward compatibility |
| /** | ||
| * Gallery configuration extended with legacy fields for backward compatibility. | ||
| * Spec fields from GalleryConfigSchema take priority; legacy fields serve as fallbacks. | ||
| */ | ||
| export type ListViewGalleryConfig = GalleryConfig & { | ||
| /** Legacy: image field (deprecated, use coverField) */ | ||
| imageField?: string; | ||
| /** Legacy: subtitle field */ | ||
| subtitleField?: string; | ||
| [key: string]: any; | ||
| }; |
There was a problem hiding this comment.
According to Rule #-1 (English-Only Codebase), all user-facing text in comments, documentation, and code MUST be written in English for international accessibility. The PR title and issue description contain Chinese text ("标准化接入"), which should be translated to English. While this is a critical codebase convention, the actual code implementation here is correct and properly documented in English.
| case 'gallery': { | ||
| // Merge spec config over legacy options into nested gallery prop | ||
| const mergedGallery = { | ||
| ...(schema.options?.gallery || {}), | ||
| ...(schema.gallery || {}), | ||
| }; | ||
| return { | ||
| type: 'object-gallery', | ||
| ...baseProps, | ||
| // Nested gallery config (spec-compliant, used by ObjectGallery) | ||
| gallery: Object.keys(mergedGallery).length > 0 ? mergedGallery : undefined, | ||
| // Deprecated top-level props for backward compat | ||
| imageField: schema.gallery?.coverField || schema.gallery?.imageField || schema.options?.gallery?.imageField, | ||
| titleField: schema.gallery?.titleField || schema.options?.gallery?.titleField || 'name', | ||
| subtitleField: schema.gallery?.subtitleField || schema.options?.gallery?.subtitleField, | ||
| ...(schema.gallery?.coverFit ? { coverFit: schema.gallery.coverFit } : {}), | ||
| ...(schema.gallery?.cardSize ? { cardSize: schema.gallery.cardSize } : {}), | ||
| ...(schema.gallery?.visibleFields ? { visibleFields: schema.gallery.visibleFields } : {}), | ||
| ...(groupingConfig ? { grouping: groupingConfig } : {}), | ||
| ...(schema.options?.gallery || {}), | ||
| ...(schema.gallery || {}), | ||
| }; | ||
| case 'timeline': | ||
| } | ||
| case 'timeline': { | ||
| // Merge spec config over legacy options into nested timeline prop | ||
| const mergedTimeline = { | ||
| ...(schema.options?.timeline || {}), | ||
| ...(schema.timeline || {}), | ||
| }; | ||
| return { | ||
| type: 'object-timeline', | ||
| ...baseProps, | ||
| // Nested timeline config (spec-compliant, used by ObjectTimeline) | ||
| timeline: Object.keys(mergedTimeline).length > 0 ? mergedTimeline : undefined, | ||
| // Deprecated top-level props for backward compat | ||
| startDateField: schema.timeline?.startDateField || schema.options?.timeline?.startDateField || schema.options?.timeline?.dateField || 'created_at', | ||
| titleField: schema.timeline?.titleField || schema.options?.timeline?.titleField || 'name', | ||
| ...(schema.timeline?.endDateField ? { endDateField: schema.timeline.endDateField } : {}), | ||
| ...(schema.timeline?.groupByField ? { groupByField: schema.timeline.groupByField } : {}), | ||
| ...(schema.timeline?.colorField ? { colorField: schema.timeline.colorField } : {}), | ||
| ...(schema.timeline?.scale ? { scale: schema.timeline.scale } : {}), | ||
| ...(schema.options?.timeline || {}), | ||
| ...(schema.timeline || {}), | ||
| }; | ||
| } |
There was a problem hiding this comment.
According to Rule #2 (Documentation Driven Development), any feature implemented or refactored MUST update corresponding documentation. This PR standardizes Gallery/Timeline config integration but does not update:
packages/plugin-list/README.md- Should document the new spec-compliantgalleryandtimelinefields in the schema examples and add migration guidance fromoptions.gallery/options.timelineto the new nested config pattern.packages/plugin-timeline/README.md- Should document the newtimeline.*nested config support with field examples (startDateField, endDateField, scale, etc.).content/docs/plugins/plugin-timeline.mdx- Should show examples of using ObjectTimeline with the spec-compliant nested config.
The definition of done requires that documentation reflect the new code/architecture. Users reading the documentation will not discover the new nested config pattern without these updates.
| /** | ||
| * ObjectUI | ||
| * Copyright (c) 2024-present ObjectStack Inc. | ||
| * | ||
| * This source code is licensed under the MIT license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| */ | ||
|
|
||
| import { describe, it, expect, vi, beforeEach } from 'vitest'; | ||
| import { render, screen } from '@testing-library/react'; | ||
| import '@testing-library/jest-dom'; | ||
| import { ObjectTimeline } from '../ObjectTimeline'; | ||
|
|
||
| // Mock dependencies | ||
| vi.mock('@object-ui/react', () => ({ | ||
| useDataScope: () => undefined, | ||
| useNavigationOverlay: () => ({ | ||
| isOverlay: false, | ||
| handleClick: vi.fn(), | ||
| selectedRecord: null, | ||
| isOpen: false, | ||
| close: vi.fn(), | ||
| setIsOpen: vi.fn(), | ||
| mode: 'page' as const, | ||
| width: undefined, | ||
| view: undefined, | ||
| open: vi.fn(), | ||
| }), | ||
| })); | ||
|
|
||
| vi.mock('@object-ui/components', () => ({ | ||
| NavigationOverlay: () => null, | ||
| })); | ||
|
|
||
| vi.mock('@object-ui/mobile', () => ({ | ||
| usePullToRefresh: () => ({ | ||
| ref: { current: null }, | ||
| isRefreshing: false, | ||
| pullDistance: 0, | ||
| }), | ||
| })); | ||
|
|
||
| // Mock the TimelineRenderer to inspect the schema it receives | ||
| const mockTimelineRenderer = vi.fn(() => <div data-testid="timeline-renderer" />); | ||
| vi.mock('../renderer', () => ({ | ||
| TimelineRenderer: (props: any) => mockTimelineRenderer(props), | ||
| })); | ||
|
|
||
| describe('ObjectTimeline Spec Config', () => { | ||
| beforeEach(() => { | ||
| mockTimelineRenderer.mockClear(); | ||
| }); | ||
|
|
||
| const mockData = [ | ||
| { id: '1', name: 'Event A', start_date: '2024-01-01', end_date: '2024-01-15', category: 'work', priority_color: 'red' }, | ||
| { id: '2', name: 'Event B', start_date: '2024-02-01', end_date: '2024-02-28', category: 'personal', priority_color: 'blue' }, | ||
| ]; | ||
|
|
||
| describe('nested timeline config', () => { | ||
| it('uses timeline.startDateField to resolve start dates', () => { | ||
| const schema: any = { | ||
| type: 'timeline' as const, | ||
| objectName: 'events', | ||
| timeline: { startDateField: 'start_date', titleField: 'name' }, | ||
| }; | ||
| render(<ObjectTimeline schema={schema} data={mockData} />); | ||
| expect(mockTimelineRenderer).toHaveBeenCalled(); | ||
| const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema; | ||
| expect(renderedSchema.items[0].time).toBe('2024-01-01'); | ||
| expect(renderedSchema.items[0].startDate).toBe('2024-01-01'); | ||
| }); | ||
|
|
||
| it('uses timeline.titleField to resolve titles', () => { | ||
| const schema: any = { | ||
| type: 'timeline' as const, | ||
| objectName: 'events', | ||
| timeline: { startDateField: 'start_date', titleField: 'name' }, | ||
| }; | ||
| render(<ObjectTimeline schema={schema} data={mockData} />); | ||
| const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema; | ||
| expect(renderedSchema.items[0].title).toBe('Event A'); | ||
| expect(renderedSchema.items[1].title).toBe('Event B'); | ||
| }); | ||
|
|
||
| it('uses timeline.endDateField to resolve end dates', () => { | ||
| const schema: any = { | ||
| type: 'timeline' as const, | ||
| objectName: 'events', | ||
| timeline: { startDateField: 'start_date', endDateField: 'end_date', titleField: 'name' }, | ||
| }; | ||
| render(<ObjectTimeline schema={schema} data={mockData} />); | ||
| const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema; | ||
| expect(renderedSchema.items[0].endDate).toBe('2024-01-15'); | ||
| }); | ||
|
|
||
| it('uses timeline.groupByField to add group property', () => { | ||
| const schema: any = { | ||
| type: 'timeline' as const, | ||
| objectName: 'events', | ||
| timeline: { startDateField: 'start_date', titleField: 'name', groupByField: 'category' }, | ||
| }; | ||
| render(<ObjectTimeline schema={schema} data={mockData} />); | ||
| const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema; | ||
| expect(renderedSchema.items[0].group).toBe('work'); | ||
| expect(renderedSchema.items[1].group).toBe('personal'); | ||
| }); | ||
|
|
||
| it('uses timeline.colorField to add color property', () => { | ||
| const schema: any = { | ||
| type: 'timeline' as const, | ||
| objectName: 'events', | ||
| timeline: { startDateField: 'start_date', titleField: 'name', colorField: 'priority_color' }, | ||
| }; | ||
| render(<ObjectTimeline schema={schema} data={mockData} />); | ||
| const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema; | ||
| expect(renderedSchema.items[0].color).toBe('red'); | ||
| expect(renderedSchema.items[1].color).toBe('blue'); | ||
| }); | ||
|
|
||
| it('uses timeline.scale to set timeScale on rendered schema', () => { | ||
| const schema: any = { | ||
| type: 'timeline' as const, | ||
| objectName: 'events', | ||
| timeline: { startDateField: 'start_date', titleField: 'name', scale: 'month' }, | ||
| }; | ||
| render(<ObjectTimeline schema={schema} data={mockData} />); | ||
| const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema; | ||
| expect(renderedSchema.timeScale).toBe('month'); | ||
| }); | ||
|
|
||
| it('timeline.scale supports all valid values', () => { | ||
| const scales = ['hour', 'day', 'week', 'month', 'quarter', 'year'] as const; | ||
| scales.forEach((scale) => { | ||
| mockTimelineRenderer.mockClear(); | ||
| const schema: any = { | ||
| type: 'timeline' as const, | ||
| objectName: 'events', | ||
| timeline: { startDateField: 'start_date', titleField: 'name', scale }, | ||
| }; | ||
| render(<ObjectTimeline schema={schema} data={mockData} />); | ||
| const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema; | ||
| expect(renderedSchema.timeScale).toBe(scale); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('backward compatibility', () => { | ||
| it('falls back to flat startDateField when timeline config is not set', () => { | ||
| const schema: any = { | ||
| type: 'timeline' as const, | ||
| objectName: 'events', | ||
| startDateField: 'start_date', | ||
| titleField: 'name', | ||
| }; | ||
| render(<ObjectTimeline schema={schema} data={mockData} />); | ||
| const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema; | ||
| expect(renderedSchema.items[0].time).toBe('2024-01-01'); | ||
| }); | ||
|
|
||
| it('falls back to legacy dateField when startDateField is not set', () => { | ||
| const data = [{ id: '1', name: 'Event', legacy_date: '2024-03-15' }]; | ||
| const schema: any = { | ||
| type: 'timeline' as const, | ||
| objectName: 'events', | ||
| dateField: 'legacy_date', | ||
| titleField: 'name', | ||
| }; | ||
| render(<ObjectTimeline schema={schema} data={data} />); | ||
| const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema; | ||
| expect(renderedSchema.items[0].time).toBe('2024-03-15'); | ||
| }); | ||
|
|
||
| it('falls back to flat groupByField when timeline.groupByField is not set', () => { | ||
| const schema: any = { | ||
| type: 'timeline' as const, | ||
| objectName: 'events', | ||
| startDateField: 'start_date', | ||
| titleField: 'name', | ||
| groupByField: 'category', | ||
| }; | ||
| render(<ObjectTimeline schema={schema} data={mockData} />); | ||
| const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema; | ||
| expect(renderedSchema.items[0].group).toBe('work'); | ||
| }); | ||
|
|
||
| it('falls back to flat colorField when timeline.colorField is not set', () => { | ||
| const schema: any = { | ||
| type: 'timeline' as const, | ||
| objectName: 'events', | ||
| startDateField: 'start_date', | ||
| titleField: 'name', | ||
| colorField: 'priority_color', | ||
| }; | ||
| render(<ObjectTimeline schema={schema} data={mockData} />); | ||
| const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema; | ||
| expect(renderedSchema.items[0].color).toBe('red'); | ||
| }); | ||
|
|
||
| it('falls back to flat scale when timeline.scale is not set', () => { | ||
| const schema: any = { | ||
| type: 'timeline' as const, | ||
| objectName: 'events', | ||
| startDateField: 'start_date', | ||
| titleField: 'name', | ||
| scale: 'week', | ||
| }; | ||
| render(<ObjectTimeline schema={schema} data={mockData} />); | ||
| const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema; | ||
| expect(renderedSchema.timeScale).toBe('week'); | ||
| }); | ||
|
|
||
| it('spec timeline config takes priority over flat props', () => { | ||
| const schema: any = { | ||
| type: 'timeline' as const, | ||
| objectName: 'events', | ||
| startDateField: 'end_date', // flat — should be overridden | ||
| titleField: 'id', // flat — should be overridden | ||
| groupByField: 'priority_color', // flat — should be overridden | ||
| timeline: { | ||
| startDateField: 'start_date', // spec — takes priority | ||
| titleField: 'name', // spec — takes priority | ||
| groupByField: 'category', // spec — takes priority | ||
| }, | ||
| }; | ||
| render(<ObjectTimeline schema={schema} data={mockData} />); | ||
| const renderedSchema = mockTimelineRenderer.mock.calls[0][0].schema; | ||
| expect(renderedSchema.items[0].time).toBe('2024-01-01'); | ||
| expect(renderedSchema.items[0].title).toBe('Event A'); | ||
| expect(renderedSchema.items[0].group).toBe('work'); | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
According to Guideline 1000001, after completing a development task, tests should be run and any issues addressed. The PR description mentions comprehensive test coverage (12 ObjectGallery tests, 13 ObjectTimeline tests, 10 type-level tests, 70 bridge tests), but does not provide evidence of running pnpm test to verify all tests pass. Please run pnpm test (or pnpm vitest run packages/plugin-list packages/plugin-timeline packages/types packages/react) and confirm all tests pass before merging.
Standardizes Gallery and Timeline spec config integration across ObjectUI, replacing legacy flat prop spreading with nested config objects and strong types from
@objectstack/spec/ui.Bug fix
In
ListView.tsx, both gallery and timeline cases now pass config as nested objects instead of spreading flat:Strong types
ListViewSchema.galleryand.timelinereplaced from inline object types →ListViewGalleryConfig/ListViewTimelineConfig(intersection of spec canonicalGalleryConfig/TimelineConfig+ legacy fields + index signature for backward compat)@object-ui/typeslist-view.tsupdated fromany→ strongListViewGalleryConfig/ListViewTimelineConfigtypesObjectTimeline nested config support
ObjectTimelinenow accepts nestedschema.timeline.*config (mirrors theObjectGallerypattern)timeline.startDateField→mapping.date→startDateField→dateField→'date'timeline.scale→ renderertimeScaleproperty for gantt variant renderingObjectGallery
schema.gallery.*with legacyimageField/titleFieldfallbacks — no code changes neededTests
coverField,cardSizegrid class,coverFitobject-contain,visibleFields,titleField, legacy fallbacks, spec-over-legacy prioritystartDateField,endDateField,groupByField,colorField,scale, all scale enum values, backward compat fallbacks, spec-over-flat priorityListViewSchemaaccepts spec config, legacy fields,options.*fallback, cardSize/coverFit enums, and gallery+timeline co-existenceOriginal prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.